package com.hz.tgb.data.redis.lock;

import cn.hutool.core.util.IdUtil;
import com.hz.tgb.entity.Book;
import com.hz.tgb.spring.SpringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Supplier;

/**
 * Redis分布式锁 - 单机版
 *
 * @author hezhao on 2019.11.13
 */
@Component
public class RedisLockUtil {

    /*
    为了确保分布式锁可用，我们至少要确保锁的实现同时满足以下四个条件：
    1、互斥性。在任意时刻，只有一个客户端能持有锁。
    2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁，也能保证后续其他客户端能加锁。
    3、具有容错性。只要大部分的Redis节点正常运行，客户端就可以加锁和解锁。
    4、解铃还须系铃人。加锁和解锁必须是同一个客户端，客户端自己不能把别人加的锁给解了。
     */

    private static final Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);

    private static RedisTemplate<String, Object> cacheTemplate;

    /** EX: 设置键的过期时间为秒; 等同于SETEX命令 */
    private static final String REDIS_EX = "EX";
    /** PX: 设置键的过期时间为毫秒; 等同于PSETEX命令 */
    private static final String REDIS_PX = "PX";

    /** NX: 只在键不存在时，才对键进行设置操作; 等同于SETNX命令 */
    private static final String REDIS_NX = "NX";
    /** XX: 只在键已经存在时，才对键进行设置操作 */
    private static final String REDIS_XX = "XX";

    /** OK: Redis操作是否成功 */
    private static final String REDIS_OK = "OK";
    /** 解锁是否成功 */
    private static final Long RELEASE_SUCCESS = 1L;

    /** 解锁Lua脚本 */
    private static final String UNLOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    /** >> 100000000 纳秒=100 毫秒 <<
     * The number of nanoseconds for which it is faster to spin
     * rather than to use timed park. A rough estimate suffices
     * to improve responsiveness with very short timeouts.
     */
    private static final long spinForTimeoutThreshold = 100000000L;

    /** 延迟过期时间，毫秒 */
    private static final long DELAY_EXPIRED_TIME = 200;

    /**
     * 加锁
     * @param lockKey 锁键
     * @param requestId 请求唯一标识
     * @param expireTime 缓存过期时间
     * @param unit 时间单位
     * @return true: 加锁成功, false: 加锁失败
     */
    public static boolean lock(String lockKey, String requestId, long expireTime, TimeUnit unit) {
        // 加锁和设置过期时间必须是原子操作，否则在高并发情况下或者Redis突然崩溃会导致数据错误。
        try {
            // 以毫秒作为过期时间
            long millisecond = TimeoutUtils.toMillis(expireTime, unit);
            String result;

            // 方法一：使用RedisConnection.execute
            result = execute(connection -> {
                Jedis jedis = (Jedis)connection.getNativeConnection();
                return jedis.set(lockKey, requestId, REDIS_NX, REDIS_PX, millisecond);
            });

            // 方法二：使用RedisConnectionUtils.getConnection
//            RedisConnection conn =  RedisConnectionUtils.getConnection(getRedisTemplate().getConnectionFactory());
//            Jedis jedis = (Jedis) conn.getNativeConnection();
//            result = jedis.set(lockKey, requestId, REDIS_NX, REDIS_PX, millisecond);

            return REDIS_OK.equals(result);
        } catch (Exception e) {
            logger.error("RedisLockUtil lock 加锁失败", e);
        }
        return false;
    }

    /**
     * 解锁
     * @param lockKey 锁键
     * @param requestId 请求唯一标识
     * @return true: 解锁成功, false: 解锁失败
     */
    public static boolean unLock(String lockKey, String requestId) {
        try {
            // 使用Lua脚本实现解锁的原子性，如果requestId相等则解锁
            Object result = execute(connection -> {
                Jedis jedis = (Jedis)connection.getNativeConnection();
                return jedis.eval(UNLOCK_LUA_SCRIPT, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            });
            return Objects.equals(RELEASE_SUCCESS, result);
        } catch (Exception e) {
            logger.error("RedisLockUtil unLock 解锁失败", e);
        }
        return false;
    }

    /**
     * 设置过期时间, 200毫秒
     * @param lockKey
     */
    public static boolean expire(String lockKey, String requestId) {
        return expire(lockKey, requestId, DELAY_EXPIRED_TIME, TimeUnit.MILLISECONDS);
    }

    /**
     * 设置过期时间
     * @param lockKey 锁键
     * @param timeout 过期时长
     * @param unit 时间单位
     */
    public static boolean expire(String lockKey, String requestId, long timeout, TimeUnit unit) {
        try {
            Object obj = getRedisTemplate().opsForValue().get(lockKey);
            // 如果此时lockKey已过期，则重新设置过期时间为200毫秒
            if (obj == null) {
                return lock(lockKey, requestId, timeout, unit);
            }
            // 此时锁已被别的请求拿到
            if (!Objects.equals(obj, requestId)) {
                return false;
            }
            // 设置过期时间为200毫秒
            Boolean result = getRedisTemplate().expire(lockKey, timeout, unit);
            return result == null ? false : result;
        } catch (Exception e) {
            logger.error("RedisLockUtil expire 设置过期时间失败", e);
        }
        return false;
    }

    /**
     * 阻塞锁，拿到锁后执行业务逻辑。注意：超时返回null，程序会继续往下执行
     * @param callback 业务处理逻辑，入参默认为NULL
     * @param lockKey 锁键
     * @param timeout 超时时长, 缓存过期时间默认等于超时时长
     * @param unit 时间单位
     * @return R
     */
    public static <R> R tryLock(Supplier<R> callback, String lockKey, long timeout, TimeUnit unit) {
        return tryLock(callback, lockKey, IdUtil.fastSimpleUUID(), timeout, timeout, unit, TimeOutProcess.DEFAULT);
    }

    /**
     * 阻塞锁，拿到锁后执行业务逻辑。注意：超时会抛出异常
     * @param callback 业务处理逻辑，入参默认为NULL
     * @param lockKey 锁键
     * @param timeout 超时时长, 缓存过期时间默认等于超时时长
     * @param unit 时间单位
     * @return R
     */
    public static <R> R tryLockTimeout(Supplier<R> callback, String lockKey, long timeout, TimeUnit unit) {
        return tryLock(callback, lockKey, IdUtil.fastSimpleUUID(), timeout, timeout, unit, TimeOutProcess.THROW_EXCEPTION);
    }

    /**
     * 阻塞锁，拿到锁后执行业务逻辑。注意：超时会给予补偿，即处理正常逻辑
     * @param callback 业务处理逻辑，入参默认为NULL
     * @param lockKey 锁键
     * @param timeout 超时时长, 缓存过期时间默认等于超时时长
     * @param unit 时间单位
     * @return R
     */
    public static <R> R tryLockCompensate(Supplier<R> callback, String lockKey, long timeout, TimeUnit unit) {
        return tryLock(callback, lockKey, IdUtil.fastSimpleUUID(), timeout, timeout, unit, TimeOutProcess.CARRY_ON);
    }

    /**
     * 阻塞锁，拿到锁后执行业务逻辑
     * @param callback 业务处理逻辑
     * @param lockKey 锁键
     * @param requestId 请求唯一标识
     * @param timeout 超时时长
     * @param expireTime 缓存过期时间
     * @param unit 时间单位
     * @param timeoutProceed 超时处理逻辑
     * @return R
     */
    public static <R> R tryLock(Supplier<R> callback, String lockKey, String requestId,
                                long timeout, long expireTime, TimeUnit unit, TimeOutProcess timeoutProceed) {
        boolean lockFlag = false;
        try {
            lockFlag = tryLock(lockKey, requestId, timeout, expireTime, unit);
            if(lockFlag){
                return callback.get();
            }
        } finally {
            if (lockFlag){
                // unLock(lockKey, requestId);
                // 不能立即删除，而是将过期时间设置为一个短暂的值，避免下个请求马上拿到锁，导致查库查不到。
                expire(lockKey, requestId);
            }
        }
        if (timeoutProceed == null) {
            return null;
        }
        if (timeoutProceed == TimeOutProcess.THROW_EXCEPTION) {
            throw new RedisLockTimeOutException();
        }
        if (timeoutProceed == TimeOutProcess.CARRY_ON) {
            return callback.get();
        }
        return null;
    }

    /**
     * 阻塞锁
     * @param lockKey 锁键
     * @param requestId 请求唯一标识
     * @param timeout 超时时长, 缓存过期时间默认等于超时时长
     * @param unit 时间单位
     * @return true: 加锁成功, false: 加锁失败
     */
    public static boolean tryLock(String lockKey, String requestId, long timeout, TimeUnit unit) {
        return tryLock(lockKey, requestId, timeout, timeout, unit);
    }

    /**
     * 阻塞锁
     * @param lockKey 锁键
     * @param requestId 请求唯一标识
     * @param timeout 超时时长
     * @param expireTime 缓存过期时间
     * @param unit 时间单位
     * @return true: 加锁成功, false: 加锁失败
     */
    public static boolean tryLock(String lockKey, String requestId, long timeout, long expireTime, TimeUnit unit) {
        long nanosTimeout = unit.toNanos(timeout);
        if (nanosTimeout <= 0L) {
            return false;
        }
        final long deadline = System.nanoTime() + nanosTimeout;
        for (;;) {
            // 获取到锁
            if (lock(lockKey, requestId, expireTime, unit)) {
                return true;
            }
            // 判断是否需要继续阻塞， 如果已超时则返回false
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L) {
                return false;
            }
            // 休眠100毫秒
            if (nanosTimeout > spinForTimeoutThreshold) {
                LockSupport.parkNanos(spinForTimeoutThreshold);
            } else {
                LockSupport.parkNanos(nanosTimeout);
            }
        }
    }

    public static <T> T execute(RedisCallback<T> action) {
        return getRedisTemplate().execute(action);
    }

    public static RedisTemplate<String, Object> getRedisTemplate() {
        if (cacheTemplate == null) {
            cacheTemplate = SpringUtils.getBean("redisTemplate", RedisTemplate.class);
        }
        return cacheTemplate;
    }

    public static void main(String[] args) {
        Book param = new Book();
        param.setBookId(1234);
        param.setName("西游记");

        Boolean flag = tryLock(() -> {
            int bookId = param.getBookId();
            System.out.println(bookId);
            // TODO ...
            return true;
        }, "BOOK-" + param.getBookId(), 3, TimeUnit.SECONDS);

        System.out.println(flag);
    }

    /**
     * 超时处理逻辑
     */
    public enum TimeOutProcess {
        /** 默认，超时返回null，程序会继续往下执行 */
        DEFAULT,
        /** 超时会抛出异常 */
        THROW_EXCEPTION,
        /** 超时会给予补偿，即处理正常逻辑 */
        CARRY_ON,

    }

}
